package com.dogegg.lbblog.intercept;

import com.dogegg.lbblog.support.*;     
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LogRecordInterceptor implements MethodInterceptor, ApplicationContextAware, BeanFactoryAware {

    private final LogRecordCachedExpressionEvaluator evaluator;
    private LogProcessor defaultProcessor = new DefaultLogProcessor();
    private final LogRecordOperationSource logRecordOperationSource;
    private final UserDetailService userService;

    private ApplicationContext applicationContext;
    private BeanFactory beanFactory;

    public LogRecordInterceptor(LogRecordOperationSource logRecordOperationSource,
                                UserDetailService userService) {
        this.logRecordOperationSource = logRecordOperationSource;
        this.userService = userService;
        this.evaluator = new LogRecordCachedExpressionEvaluator();
    }



    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        try {
            LogRecordContext.span();
            Object result = invocation(invocation);
            processLogRecord(invocation, result, null);
            return result;
        } catch (Throwable throwable) {
            processLogRecord(invocation, null, throwable);
            throw throwable;
        } finally {
            LogRecordContext.clear();
        }
    }

    private Object invocation(MethodInvocation invocation) throws Throwable {
        return invocation.proceed();
    }

    private void processLogRecord(MethodInvocation invocation, Object result, Throwable throwable) {
        try {
            Method method = invocation.getMethod();
            Object target = invocation.getThis();
            Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);

            LogRecordOperation operation = logRecordOperationSource.getCachedOperation(method, targetClass, true);
            LogRecordOperationContext context = createOperationContext(operation, method,
                    invocation.getArguments(), target, targetClass);
            context.process(result, throwable);
        } catch (Exception e) {
            throw new LogInterceptorException(e);
        }
    }

    protected LogRecordOperationContext createOperationContext(LogRecordOperation operation,
                                                               Method method, Object[] args, Object target, Class<?> targetClass) {
        LogRecordOperationMetadata metadata = getLogRecordOperationMetadata(operation, method, targetClass);
        return new LogRecordOperationContext(metadata, args, target);
    }

    protected LogRecordOperationMetadata getLogRecordOperationMetadata(LogRecordOperation operation,
                                                                       Method method, Class<?> targetClass) {
        return new LogRecordOperationMetadata(operation, method, targetClass);
    }

    protected LogProcessor getLogProcessor() {
        return defaultProcessor;
    }

    public void setProcessor(LogProcessor defaultProcessor) {
        this.defaultProcessor = defaultProcessor;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    protected static class LogRecordOperationMetadata {
        private final LogRecordOperation operation;
        private final Method method;
        private final Class<?> targetClass;
        private final Method targetMethod;
        private final AnnotatedElementKey methodKey;

        public LogRecordOperationMetadata(LogRecordOperation operation, Method method, Class<?> targetClass) {
            this.operation = operation;
            this.targetClass = targetClass;
            this.method = BridgeMethodResolver.findBridgedMethod(method);
            this.targetMethod = (!Proxy.isProxyClass(targetClass) ?
                    AopUtils.getMostSpecificMethod(method, targetClass) : this.method);
            this.methodKey = new AnnotatedElementKey(targetMethod, targetClass);
        }
    }

    protected final class LogRecordOperationContext {
        private final LogRecordOperationMetadata metadata;
        private final Object[] args;
        private final Object target;

        public LogRecordOperationContext(LogRecordOperationMetadata metadata, Object[] args, Object target) {
            this.metadata = metadata;
            this.args = extractArgs(metadata.method, args);
            this.target = target;
        }

        private Object[] extractArgs(Method method, Object[] args) {
            if (!method.isVarArgs()) {
                return args;
            }
            Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
            Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
            System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
            System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
            return combinedArgs;
        }

        private void process(Object result, Throwable throwable) {
            LogRecordEvaluationContext evaluationContext = createEvaluationContext(result, throwable);
            if (shouldProcess(evaluationContext)) {
                processRecord(evaluationContext);
            }
        }

        private boolean shouldProcess(LogRecordEvaluationContext evaluationContext) {
            return !StringUtils.hasText(metadata.operation.getCondition()) ||
                    evaluator.condition(metadata.operation.getCondition(),
                            metadata.methodKey, evaluationContext);
        }

        private void processRecord(LogRecordEvaluationContext evaluationContext) {
            LogProcessRecord record = createLogRecord(evaluationContext);
            getProcessor().process(record);
        }

        private LogProcessRecord createLogRecord(LogRecordEvaluationContext evaluationContext) {
            LogProcessRecord record = new LogProcessRecord();
            record.setSource(metadata.operation.getSource());
            record.setLabels(metadata.operation.getLabels());
            record.setContent(getContent(evaluationContext));
            record.setUser(userService.getUser());
            record.setVariable(LogRecordContext.getVariableValues());
            return record;
        }

        private String getContent(LogRecordEvaluationContext evaluationContext) {
            return StringUtils.hasText(metadata.operation.getContent()) ?
                    evaluator.content(metadata.operation.getContent(),
                            metadata.methodKey, evaluationContext) : null;
        }

        private LogProcessor getProcessor() {
            if (StringUtils.isEmpty(metadata.operation.getProcessor())) {
                return getLogProcessor();
            }
            LogProcessor processor = beanFactory.getBean(
                    metadata.operation.getProcessor(), LogProcessor.class);
            if (processor == null) {
                throw new IllegalStateException("No processor could be found");
            }
            return processor;
        }

        private LogRecordEvaluationContext createEvaluationContext(Object ret, Throwable throwable) {
            return evaluator.createEvaluationContext(metadata.method, args, target,
                    ret, throwable, applicationContext, userService);
        }
    }
}